<?php
declare(strict_types=1);

namespace Zms\Unit;

use Exception;
use Zms\Unit\Library\ArrayToXml;

/**
 * 数组工具类
 */
class Arr
{
    /**
     * 无限多维数组合并，在原有的基础上由右侧的值合并前面的值。
     * 此静态方法用于合并多个数组。如果遇到相同键名且对应的值都是数组，
     * 则会递归地合并这些子数组；否则，右侧的值将覆盖左侧的值。
     * 注意：此函数假定所有输入都是数组。如果提供了非数组类型的参数，行为可能不符合预期。
     * @param array ...$arrays 可变数量的数组参数。每个参数都应为一个数组。
     * @return array 返回合并后的数组。该数组包含了所有传入数组的数据，其中后出现的数组中的数据优先级更高。
     * 示例:
     * $array1 = ['a' => 1, 'b' => ['x' => 10]];
     * $array2 = ['a' => 2, 'b' => ['y' => 20]];
     * 调用 self::merge($array1, $array2) 将返回 ['a' => 2, 'b' => ['x' => 10, 'y' => 20]]
     * $arr1 = ['a'];
     * $arr2 = ['b'];
     * 调用 self::merge($arr1, $arr2) 将返回 ['b']
     */
    public static function merge(...$arrays): array
    {
        $array = [];
        foreach ($arrays as $item) {
            if (!is_array($item)) return $item; // 这里建议处理非数组输入以避免意外错误
            foreach ($item as $key => $info) {
                if (is_array($info)) {
                    if (isset($array[$key]) && $array[$key]) {
                        $array[$key] = self::merge($array[$key], $info);
                        continue;
                    }
                }
                $array[$key] = $info;
            }
        }
        return $array;
    }


    /**
     * 将一个平面的数组转换为树状结构。
     * 此方法接收一个包含节点信息的数组，并根据指定的键名（如'id', 'pid'）重组这个数组，
     * 生成一个以特定根节点（默认为0）为起点的树状结构。每个节点可以有一个或多个子节点，
     * 子节点通过$child参数指定的键名来标识。
     *
     * @param array $array 包含所有节点的原始数组。每个元素应该至少包含'id'和'pid'两个键。
     * @param string $findName 数组中用于唯一标识节点的键名，默认为'id'。
     * @param string|int $pid 数组中表示父节点ID的键名，默认为'pid'。
     * @param string $child 输出数组中用于存储子节点的键名，默认为'children'。
     * @param int|string $root 根节点的值，默认为0。它决定了哪些节点被视为顶层节点。
     *
     * @return array 返回重组后的树状结构数组。如果找不到匹配的根节点，则返回空数组。
     *
     * 示例:
     * 输入: [['id' => 1, 'pid' => 0], ['id' => 2, 'pid' => 1], ['id' => 3, 'pid' => 1]]
     * 调用 self::getTree($array) 将返回 [[['id' => 1, 'pid' => 0, 'children' => [['id' => 2, 'pid' => 1], ['id' => 3, 'pid' => 1]]]]]
     */
    public static function getTree($array, $findName = 'id', $pid = 'pid', $child = 'children', $root = 0): array
    {
        $tree = [];
        $packData = [];
        foreach ($array as $data) {
            $packData[$data[$findName]] = $data;
        }
        foreach ($packData as $key => $val) {
            if ($val[$pid] == $root) {
                $tree[] = &$packData[$key];
            } else {
                $packData[$val[$pid]][$child][] = &$packData[$key];
            }
        }
        return $tree;
    }


    /**
     * @param array $data 所有节点数据组成的数组
     * @param int $pid 当前层级的父级 ID（用于递归调用）
     * @param string $valKey 主键字段名（如 'id'）
     * @param string $nameKey 名称字段名（如 'name'）
     * @param string $parentKey 父级字段名（如 'pid'）
     * @param string $str 层级结构符号（用于显示层级缩进效果）
     * @param int $index 递归深度（用于控制层级缩进长度）
     *
     * @return array 返回格式化后的树状结构数组，每个节点名称前加上层级符号
     * 数据样例
     * $data = [
     *      ['id' => 1, 'pid' => 0, 'name' => '首页'],
     *      ['id' => 2, 'pid' => 0, 'name' => '产品'],
     *      ['id' => 3, 'pid' => 2, 'name' => '手机'],
     *      ['id' => 4, 'pid' => 2, 'name' => '电脑'],
     *      ['id' => 5, 'pid' => 4, 'name' => '笔记本'],
     *      ['id' => 6, 'pid' => 0, 'name' => '关于我们']
     * ];
     * 生成内容
     * [
     *      ['id' => 1, 'pid' => 0, 'name' => '|--------首页'],
     *      ['id' => 2, 'pid' => 0, 'name' => '|--------产品'],
     *      ['id' => 3, 'pid' => 2, 'name' => '|--------|--------手机'],
     *      ['id' => 4, 'pid' => 2, 'name' => '|--------|--------电脑'],
     *      ['id' => 5, 'pid' => 4, 'name' => '|--------|--------|--------笔记本'],
     *      ['id' => 6, 'pid' => 0, 'name' => '|--------关于我们'],
     * ]
     */
    public static function formatMenuAsTree($data, $pid = 0, $valKey = 'id', $nameKey = 'name', $parentKey = 'pid', $str = '|--------', $index = 0): array
    {
        static $list = []; // 使用静态变量保存结果，避免重复初始化
        if ($index === 0) {
            $list = []; // 第一次调用时清空静态变量
        }
        foreach ($data as $v) {
            // 构建一个映射关系：父ID => 子ID列表（可选，当前未使用）
            $idlist[$v[$parentKey]][] = $v[$valKey];
            // 如果当前项的父ID等于指定的 pid，则处理该节点
            if ($v[$parentKey] === $pid) {
                // 在节点名称前添加层级结构符号
                $v[$nameKey] = str_repeat($str, $index) . $v[$nameKey];

                // 添加到最终结果中
                $list[] = $v;
                // 递归处理子级节点，将当前节点的 id 作为新的 pid
                self::formatMenuAsTree($data, $v[$valKey], $valKey, $nameKey, $parentKey, $str, $index + 1);
            }
        }
        return $list;
    }


    /**
     * @param array $data 所有节点数据组成的数组的map
     * @param int $pid 当前层级的父级 ID（用于递归调用）
     * @param string $valKey 主键字段名（如 'id'）
     * @param string $nameKey 名称字段名（如 'name'）
     * @param string $parentKey 父级字段名（如 'pid'）
     * @param string $str 层级结构符号（用于显示层级缩进效果）
     * @param int $index 递归深度（用于控制层级缩进长度）
     *
     * @return array 返回格式化后的树状结构数组，每个节点名称前加上层级符号
     * 数据样例
     * $data = [
     *      ['id' => 1, 'pid' => 0, 'name' => '首页'],
     *      ['id' => 2, 'pid' => 0, 'name' => '产品'],
     *      ['id' => 3, 'pid' => 2, 'name' => '手机'],
     *      ['id' => 4, 'pid' => 2, 'name' => '电脑'],
     *      ['id' => 5, 'pid' => 4, 'name' => '笔记本'],
     *      ['id' => 6, 'pid' => 0, 'name' => '关于我们']
     * ];
     * 生成内容
     * [
     *      ['1'=> '|--------首页'],
     *      ['2'=> '|--------产品'],
     *      ['3'=> '|--------|--------手机'],
     *      ['4'=> '|--------|--------电脑'],
     *      ['5'=> '|--------|--------|--------笔记本'],
     *      ['6'=> '|--------关于我们'],
     * ]
     */
    public static function toTreeMap($data, $pid = 0, $valKey = 'id', $nameKey = 'name', $parentKey = 'pid', $str = '|--------', $index = 0): array
    {
        $result = self::formatMenuAsTree($data, $pid, $valKey, $nameKey, $parentKey, $str, $index);
        return Arr::map($result, $valKey, $nameKey);
    }

    /**
     * 将给定的映射（map）转换为包含'value'和'label'键的数组模式。
     *
     * 此方法主要用于将如关联数组等映射数据结构转换成表单可识别的数据格式，
     * 例如下拉列表选项。输出数组中的每一个元素都将包含'value'和'label'两个键。
     *
     * @param array $map 需要转换的原始映射数据，通常是一个关联数组。
     *                           示例：['key1' => 'Label 1', 'key2' => 'Label 2']
     * @param  $valueType 指定'value'字段期望的数据类型。可以是以下之一：
     *                               - null: 默认行为，保持原样（可能是字符串或数字）。
     *                               - 'string': 强制所有'value'字段为字符串类型。
     *                               - 'int': 强制所有'value'字段为整数类型。
     *
     * @return array             返回一个由映射数据转换而来的数组，每个元素都具有'value'和'label'键。
     *                           示例输出：[['value' => 'key1', 'label' => 'Label 1'], ['value' => 'key2', 'label' => 'Label 2']]
     *
     * 注意：如果$valueType设置为'int'但转换失败（比如尝试将非数字字符串转为整数），则保留原始值。
     */
    public static function mapToFormData($map, $valueType = null): array
    {
        $data = [];
        foreach ($map as $value => $item) {
            if ($valueType === 'string') {
                $data[] = ['value' => (string)$value, 'label' => $item];
            } elseif ($valueType === 'string') {
                $data[] = ['value' => (int)$value, 'label' => $item];
            } else {
                $data[] = ['value' => $value, 'label' => $item];
            }
        }
        return $data;
    }

    /**
     * 对数组进行MD5哈希处理。
     * 此方法首先将数组转换为查询字符串格式，然后对该字符串执行MD5哈希操作。
     * 可以选择生成32字符长度的标准MD5哈希值或16字符长度的简短版本。
     * @param array $array 需要进行哈希处理的数组。
     * @param bool $len_16 如果设置为true，则返回16字符长度的MD5哈希值；否则返回32字符长度的完整MD5哈希值。默认为false。
     * @return string 返回数组经过MD5哈希处理后的字符串。
     * 示例:
     * 输入: ['key1' => 'value1', 'key2' => 'value2']
     * 调用 self::md5($array) 将返回一个32字符长度的MD5哈希值。
     * 调用 self::md5($array, true) 将返回一个16字符长度的MD5哈希值。
     * 注意:
     * - 数组中的键和值会被编码为URL查询字符串格式（例如：key1=value1&key2=value2）后再进行哈希计算。
     * - 由于http_build_query()会进行URL编码，这可能会影响最终的哈希结果。
     */
    public static function md5(array $array, bool $len_16 = false): string
    {
        $json = http_build_query($array);
        return $len_16 ? substr(md5($json), 8, 16) : md5($json);
    }


    /**
     * 此方法可以从给定的数组中提取所有值或特定键对应的值。
     * 如果$key参数为null，则返回数组中所有的值（忽略键）；
     * 否则，返回指定键对应的所有值组成的数组。
     *
     * @param array $array 需要从中提取值的数组。
     * @param string|int|null $key 可选。指定需要提取值的键名或键值。如果为null，则返回数组的所有值。
     * @return array 返回一个由数组中的值组成的数组。如果提供了$key，则仅返回该键对应的所有值。
     *
     * 示例:
     * 输入: [['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob']]
     * 调用 self::getValues($array) 将返回 [0 => ['id' => 1, 'name' => 'Alice'], 1 => ['id' => 2, 'name' => 'Bob']]
     * 调用 self::getValues($array, 'name') 将返回 [0 => 'Alice', 1 => 'Bob']
     *
     * 注意:
     * - 如果数组中的某些元素不包含指定的$key，则这些元素在结果数组中将被忽略。
     * - 当$key为null时，结果数组保留原始数组的索引顺序；否则，结果数组将重置索引为连续的数字索引。
     */
    public static function getValues(array $array, $key = null): array
    {
        if (is_null($key)) {
            return array_values($array);
        }
        return array_column($array, $key);
    }


    /**
     * 将数组映射为键值对数组。
     * @param array $array 需要进行映射的数组。数组中的每个元素可以是对象或者关联数组。
     * @param string $keyName 数组中用于作为新数组键名的键，默认为'id'。
     * @param string|null $valName 数组中用于作为新数组值的键。如果为null，则使用整个元素作为值；如果为'*'，也使用整个元素作为值。
     * @return array 返回映射后的新数组，其中键为$keyName对应的值，值根据$valName参数确定。
     *
     * 示例:
     * 输入: [['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob']]
     * 调用 self::map($array) 将返回 [1 => 'Alice', 2 => 'Bob']
     * 调用 self::map($array, 'id', '*') 将返回 [1 => ['id' => 1, 'name' => 'Alice'], 2 => ['id' => 2, 'name' => 'Bob']]
     * 调用 self::map($array, 'id', null) 将返回 [1 => ['id' => 1, 'name' => 'Alice'], 2 => ['id' => 2, 'name' => 'Bob']]
     */
    public static function map($array, $keyName = 'id', $valName = 'name'): array
    {
        $data = [];
        foreach ($array as $row) {
            $row = Obj::toArray($row);
            if (is_null($valName)) {
                $data[$row[$keyName]] = $row;
            } else {
                $data[$row[$keyName]] = ($valName === '*' ? $row : $row[$valName]);
            }
        }
        return $data;
    }


    /**
     * 从数组中获取指定键的值，支持通过点符号访问深层嵌套的数组。
     * 此方法允许使用点符号（如 'a.b.c'）来访问多维数组中的深层数据。
     * 如果路径中的任何部分不存在，则返回默认值。
     *
     * @param array $array 需要从中获取值的数组。
     * @param string $key 键名或使用点符号表示的键路径。例如：'user.name'。
     * @param mixed $default 可选。如果指定的键路径未找到，则返回该默认值。默认为null。
     * @return mixed 返回找到的值或提供的默认值。
     * 示例:
     * 输入: ['user' => ['name' => 'Alice', 'age' => 30]]
     * 调用 self::get($array, 'user.name') 将返回 'Alice'
     * 调用 self::get($array, 'user.age', 18) 将返回 30
     * 调用 self::get($array, 'user.email', 'no-email@example.com') 将返回 'no-email@example.com'
     *
     * 注意:
     * - 使用Str::explode('.')对$key进行分割处理，假设Str类中有explode静态方法用于字符串分割。
     * - 确保传入的$array是有效的数组，并且$key是一个有效的字符串。
     */
    public static function get(array $array, $key, $default = null)
    {
        if (!(is_string($key) && $key)) {
            return $default;
        }
        $keys = Str::explode('.', trim($key));
        foreach ($keys as $k) {
            if (is_array($array) && isset($array[$k])) {
                $array = $array[$k];
            } else {
                return $default;
            }
        }
        return $array;
    }


    /**
     * @describe 检查指定键值数据是否存在
     * @user zlf <zms-pro@qq.com> 2025/10/23
     * @param $array
     * @param $key
     * @return bool
     */
    public static function has($array, $key): bool
    {
        if (!(is_string($key) && $key)) {
            return false;
        }
        $keys = Str::explode('.', trim($key));
        foreach ($keys as $k) {
            if (is_array($array) && isset($array[$k])) {
                $array = $array[$k];
            } else {
                return false;
            }
        }
        return true;
    }


    /**
     * @describe 设置数组值
     * @user zlf <zms-pro@qq.com> 2025/10/31
     * @param array $array
     * @param $key
     * @param $value
     * @return array
     */
    public static function set(array &$array, $key, $value): array
    {
        if (is_null($key)) {
            return $array = $value;
        }
        if (!is_string($key)) {
            $array[$key] = $value;
            return $array;
        }
        $keys = explode('.', $key);
        while (count($keys) > 1) {
            $key = array_shift($keys);
            if (!isset($array[$key]) || !is_array($array[$key])) {
                $array[$key] = [];
            }
            $array = &$array[$key];
        }
        $array[array_shift($keys)] = $value;
        return $array;
    }


    /**
     * 获取数组中的最后一个元素。
     * 此方法用于返回给定数组的最后一个元素。如果数组为空，则返回null。
     *
     * @param array $array 需要获取最后一个元素的数组。
     * @return mixed 返回数组的最后一个元素，如果数组为空则返回null。
     * 示例:
     * 输入: [1, 2, 3, 4]
     * 调用 self::finalValue($array) 将返回 4
     *
     * 输入: []
     * 调用 self::finalValue($array) 将返回 null
     * 注意:
     * - 此函数不会修改原数组的内部指针位置。
     * - 如果数组中只有一个元素，该元素将被返回。
     */
    public static function finalValue(array $array)
    {
        if (count($array) === 0) {
            return null;
        }
        return end($array);
    }


    /**
     * 获取数组中的第一个元素。
     * 此方法用于返回给定数组的第一个元素。如果数组为空，则返回null。
     * 通过检查数组是否为空，并使用`current()`函数来获取数组的第一个元素，
     * 避免了遍历整个数组，提高了效率。
     *
     * @param array $array 需要获取第一个元素的数组。
     * @return mixed 返回数组的第一个元素，如果数组为空则返回null。
     *
     * 示例:
     * 输入: [1, 2, 3, 4]
     * 调用 self::firstValue($array) 将返回 1
     *
     * 输入: []
     * 调用 self::firstValue($array) 将返回 null
     */
    public static function firstValue(array $array)
    {
        if (count($array) === 0) {
            return null;
        }
        return current($array);
    }


    /**
     * 获取数组最后一个键名称
     * @param array $array
     * @return int|string|null
     */
    public static function finalKey(array $array)
    {
        if (count($array)) {
            return self::finalValue(array_keys($array));
        }
        return null;
    }


    /**
     * 获取数组第一个元素名
     * @param array $array
     * @return int|string|null
     */
    public static function firstKey(array $array)
    {
        if (count($array)) {
            return self::firstValue(array_keys($array));
        }
        return null;
    }


    /**
     * 判断数组是列表还是字典
     * @param array $arr
     * @return string
     */
    public static function type(array $arr): string
    {
        return Is::list($arr) ? 'list' : 'map';
    }


    /**
     * 清理多维数组，移除所有空元素（包括空数组）。
     * 此方法递归地遍历一个多维数组，并删除所有值为空的元素。
     * 如果一个子数组在清理后变为空，则该子数组也会从其父数组中移除。
     *
     * @param array $arr 需要清理的多维数组。
     * @return array 返回清理后的数组，其中所有空值和空数组已被移除。
     *
     * 示例:
     * 输入: ['a' => 1, 'b' => [], 'c' => ['x' => null, 'y' => 2]]
     * 调用 self::cleanMultidimensionalArray($array) 将返回 ['a' => 1, 'c' => ['y' => 2]]
     */
    public static function cleanMultidimensionalArray($arr): array
    {
        foreach ($arr as $k => $v) {
            if (is_array($v) && count($v) > 0) {
                $arr[$k] = self::cleanMultidimensionalArray($v);
            }
            if (Is::empty($v)) unset($arr[$k]);
        }
        return $arr;
    }


    /**
     * 将数组中的元素连接成一个字符串，并可选择跳过空值。
     *
     * 此方法接收一个数组，根据提供的分隔符将数组中的元素连接成一个字符串。
     * 可以选择是否跳过被视为“空”的元素（如null、''、0等）。
     * @param array $list 需要连接的数组。数组可以包含任何类型的元素。
     * @param string $implode 用于连接数组元素的字符串，默认为空字符串。
     * @param bool $empty_skip 如果设置为true，则跳过所有被视为“空”的元素；如果设置为false，则包含这些元素。默认为true。
     * @return string 返回由数组元素组成的字符串，元素之间通过指定的分隔符连接。
     *
     * 示例:
     * 输入: ['a', '', 'b', null, 'c']
     * 调用 self::join($array, ',', true) 将返回 "a,b,c"
     * 调用 self::join($array, ',', false) 将返回 "a,,b,,c"
     */
    public static function join(array $list, string $implode = '', bool $empty_skip = true): string
    {
        $listData = [];
        foreach ($list as $value) {
            if (Is::empty($value) && $empty_skip === true) {
                continue;
            }
            $listData[] = $value;
        }
        return implode($implode, $listData);
    }


    /**
     * 将一个或多个元素添加到数组的末尾，并返回更新后的数组。
     * 此方法接收一个数组以及可变数量的额外参数（元素），并将这些元素逐一追加到数组的末尾。
     * 最终返回包含新旧元素的数组。
     *
     * @param array $array 需要添加元素的基础数组。
     * @param mixed ...$data 可变数量的参数。每个参数代表需要添加到数组末尾的一个元素。
     * @return array 返回包含了原数组元素及新增元素的新数组。
     *
     * 示例:
     * 输入: [1, 2], 3, 4
     * 调用 self::push($array, 3, 4) 将返回 [1, 2, 3, 4]
     */
    public static function push(array $array, ...$data): array
    {
        foreach ($data as $item) {
            $array[] = $item;
        }
        return $array;
    }


    /**
     * 移除数组中的重复值，并重置数组的键。
     * 此方法接收一个数组，移除其中的所有重复值，然后返回一个新的数组，
     * 该数组不仅去除了重复项，还通过 `array_values` 函数重置了数组的索引顺序，
     * 确保结果数组的键是从0开始的连续整数。
     *
     * @param array $arr 需要去重的数组。
     * @return array 返回一个没有重复值且键重新排序的新数组。
     *
     * 示例:
     * 输入: [1, '2', 2, '2', 3, 1]
     * 调用 self::unique($array) 将返回 [1, '2', 2, 3]
     */
    public static function unique(array $arr): array
    {
        return array_values(array_unique($arr));
    }


    /**
     * 从数组中移除指定的一个或多个键对应的元素。
     * 此方法接收一个数组以及一个键或一组键，并从该数组中删除这些键对应的元素。
     * 如果提供的键不存在于数组中，则忽略这些键。最终返回处理后的数组。
     *
     * @param array $array 需要从中移除元素的数组。
     * @param mixed $keys 要移除的键。可以是一个单独的键（字符串或整数），也可以是一个包含多个键的数组。
     * @return array 返回移除了指定键对应元素后的新数组。
     *
     * 示例:
     * 输入: ['a' => 1, 'b' => 2, 'c' => 3], 'b'
     * 调用 self::remove($array, 'b') 将返回 ['a' => 1, 'c' => 3]
     *
     * 输入: ['a' => 1, 'b' => 2, 'c' => 3], ['a', 'c']
     * 调用 self::remove($array, ['a', 'c']) 将返回 ['b' => 2]
     */
    public static function remove($array, $keys): array
    {
        $keys = is_array($keys) ? $keys : [$keys];
        foreach ($keys as $key) {
            unset($array[$key]);
        }
        return $array;
    }


    /**
     * 数组转xml
     * @param array $array 要转换的数据
     * @param string|array $rootElement 根节点
     * @param bool $replaceSpacesByUnderScoresInKeyNames
     * @param string $xmlEncoding
     * @param string $xmlVersion
     * @param array $domProperties
     * @return string
     * @throws Exception
     */
    public static function toXml(
        array  $array,
               $rootElement = '',
        bool   $replaceSpacesByUnderScoresInKeyNames = true,
        string $xmlEncoding = 'UTF-8',
        string $xmlVersion = '1.0',
        array  $domProperties = []
    ): string
    {
        return (new ArrayToXml(
            $array,
            $rootElement,
            $replaceSpacesByUnderScoresInKeyNames,
            $xmlEncoding,
            $xmlVersion,
            $domProperties
        ))->parse();
    }


    /**
     * @desc 接收返回需要的数据
     * @param array $keys
     * @param array $data
     * @param array $default
     * @return array
     * @author 竹林风 zms-pro@qq.com  2025-11-26
     */
    public static function receive(array $keys, array $data, array $default = []): array
    {
        $receive = [];
        foreach ($keys as $key) {
            $value = self::get($data, $key, self::get($default, $key));
            if (!is_null($value)) {
                self::set($receive, $key, $value);
            }
        }
        return $receive;
    }
}

